import { Callout } from "nextra-theme-docs";

# 动态 flag

GZCTF 自带对于动态 flag 分发的支持，将会在容器启用时采用 `GZCTF_FLAG` 环境变量进行注入。

<Callout type="info">

采用此环境变量的主要原因是出于防止 GZCTF 被商业滥用的考虑，因此短时间内不会开放此功能的自定义。

</Callout>

## 配置规则

在动态题目的 flag 及附件管理页面中，flag 模板将作为生成动态 flag 的依据，具有如下的规则：

1. 留空以生成随机 `GUID` 作为 flag
2. 指定 `[GUID]` 则会 **仅** 替换此处的占位符为随机 GUID
3. 若指定 `[TEAM_HASH]` 则它将会被替换为队伍 Token 与相关信息所生成的哈希值
4. 若未指定 `[TEAM_HASH]` 则将启用 Leet 字符串功能，将会基于模版对花括号内字符串进行变换，需要确保 flag 模版字符串的熵足够高
5. 若需要在指定 `[TEAM_HASH]` 的情况下启用 Leet 字符串功能，请在 flag 模版字符串之前添加 `[LEET]` 标记，此时不会检查 flag 模版字符串的熵
6. 若需要在 Leet 字符串时启用特殊字符（可能会导致注入问题），请在 flag 模版字符串之前添加 `[CLEET]` 标记

## 规则示例

1. 留空会得到 `flag{1bab71b8-117f-4dea-a047-340b72101d7b}`
2. `MyCTF{[GUID]}` 会得到 `MyCTF{1bab71b8-117f-4dea-a047-340b72101d7b}`
3. `flag{hello world}` 会得到 `flag{He1lo_w0r1d}`
4. `[CLEET]flag{hello sara}` 会得到 `flag{He1!o_$@rA}`
5. `flag{hello_world_[TEAM_HASH]}` 会得到 `flag{hello_world_5418ce4d815c}`
6. `[LEET]flag{hello world [TEAM_HASH]}` 会得到 `flag{He1lo_w0r1d_5418ce4d815c}`

## Leet 字符串

Leet 字符串是一种将字符串中的字符替换为数字或符号的方法，例如将 `a` 替换为 `4`，将 `e` 替换为 `3` 等，GZCTF 采用的 Leet 字符串规则如下：

| 字符 | 替换为 | 字符 | 替换为 | 字符 | 替换为 | 字符 | 替换为 |
| :--: | :----- | :--: | :----- | :--: | :----- | :--: | :----- |
| `A`  | `Aa4`  | `B`  | `Bb68` | `C`  | `Cc`   | `D`  | `Dd`   |
| `E`  | `Ee3`  | `F`  | `Ff1`  | `G`  | `Gg69` | `H`  | `Hh`   |
| `I`  | `Ii1l` | `J`  | `Jj`   | `K`  | `Kk`   | `L`  | `Ll1I` |
| `M`  | `Mm`   | `N`  | `Nn`   | `O`  | `Oo0`  | `P`  | `Pp`   |
| `Q`  | `Qq9`  | `R`  | `Rr`   | `S`  | `Ss5`  | `T`  | `Tt7`  |
| `U`  | `Uu`   | `V`  | `Vv`   | `W`  | `Ww`   | `X`  | `Xx`   |
| `Y`  | `Yy`   | `Z`  | `Zz2`  | `0`  | `0oO`  | `1`  | `1lI`  |
| `2`  | `2zZ`  | `3`  | `3eE`  | `4`  | `4aA`  | `5`  | `5Ss`  |
| `6`  | `6Gb`  | `7`  | `7T`   | `8`  | `8bB`  | `9`  | `9g`   |

启用复杂 Leet 字符串时，请注意字符注入问题，它采用的规则如下，由于可能性更多，达到指定的熵所需的长度会更短：

| 字符 | 替换为  | 字符 | 替换为 | 字符 | 替换为 | 字符 | 替换为  |
| :--: | :------ | :--: | :----- | :--: | :----- | :--: | :------ | --- |
| `A`  | `Aa4@`  | `B`  | `Bb68` | `C`  | `Cc(`  | `D`  | `Dd`    |
| `E`  | `Ee3`   | `F`  | `Ff1`  | `G`  | `Gg69` | `H`  | `Hh`    |
| `I`  | `Ii1l!` | `J`  | `Jj`   | `K`  | `Kk`   | `L`  | `Ll1I!` |
| `M`  | `Mm`    | `N`  | `Nn`   | `O`  | `Oo0#` | `P`  | `Pp`    |
| `Q`  | `Qq9`   | `R`  | `Rr`   | `S`  | `Ss5$` | `T`  | `Tt7`   |
| `U`  | `Uu`    | `V`  | `Vv`   | `W`  | `Ww`   | `X`  | `Xx`    |
| `Y`  | `Yy`    | `Z`  | `Zz2?` | `0`  | `0oO#` | `1`  | `1lI    | `   |
| `2`  | `2zZ?`  | `3`  | `3eE`  | `4`  | `4aA`  | `5`  | `5Ss`   |
| `6`  | `6Gb`   | `7`  | `7T`   | `8`  | `8B&`  | `9`  | `9g`    |

### 安全性

Leet 字符串的安全性取决于 flag 模版字符串的熵，对于 flag 模版中每一个字符，它都有可能被替换为多个字符。我们采用每一个可变字符的可变字符集合的长度对 2 取对数后累加，从而得到了 Leet 字符串的熵：

$$
\begin{aligned}
H &= \sum_{i=1}^{n} \log_2{m_i} \\
m_i &= \begin{cases}
    \text{len}(\text{LeetMap}[c_i]) & \text{if } c_i \text{ is in LeetMap} \\
    0 & \text{otherwise}
\end{cases}
\end{aligned}
$$

在 GZCTF 中，这一指标被限制不得低于 32，否则将会导致 flag 的安全性降低。

## 队伍哈希

队伍哈希是一种将队伍 Token 与相关信息进行哈希的方法，它将会被用于动态 flag 的生成，以保证每一个队伍都有唯一的 flag。

在 GZCTF 中，队伍哈希为 SHA256 哈希的中部 12 位，例如 `5418ce4d815c`，它将会被用于替换 flag 模版中的 `[TEAM_HASH]` 占位符。

队伍哈希的计算采用了三个参数：

- 队伍 Token：在队伍注册时由系统生成、签发的、可被公钥验证的 ed25519 签名
- 题目 ID：题目的唯一标识符
- 比赛哈希盐：加密后的比赛签名私钥加盐之后的 SHA256 哈希

生成 Team Hash 的类 python 代码如下：

```python
from hashlib import sha256

str_sha256 = lambda s: sha256(s.encode()).hexdigest()

encrypted_game_pk = "...some base64..."
chal_id = 114
team_token = "114:...some base64..."

# you can get this salt from /api/edit/games/{id}/teamhashsalt
game_salt = str_sha256(f"GZCTF@{encrypted_game_pk}@PK")

# you should calculate this hash by yourself, and put it in challenge
chal_salt = str_sha256(f"{game_salt}::{chal_id}")

# let your challenge to calculate team hash
team_hash = str_sha256(f"{chal_salt}::{team_token}")[12:24]
```

其中，比赛哈希盐 `game_salt` 可以通过管理员权限访问 `/api/edit/games/{id}/hashsalt` 接口获取，如需使用请注意保密。

### 安全性

- 队伍 Token 由 GZCTF 签发，它是一个 ed25519 签名，**可以被公钥验证，因此不会被伪造**
- 比赛哈希盐是一个比赛特异的值，源自比赛签名私钥的哈希，**需要管理员保证其安全性**
- 题目 ID 是一个整数，在创建题目时由系统生成，**由管理员结合比赛哈希盐来计算题目哈希盐**
- 题目哈希盐是一个题目特异的值，应当被用于最终的 Team Hash 计算，**其泄漏不会影响其他题目的安全性**

### 正确使用

队伍哈希的一个核心的使用场景是外部题目（队伍所访问的最终容器并非 GZCTF 所启动的容器），例如某些 Web 题目的部署难度高、依赖复杂的情况下，题目可能只有一个外部实例，而不是每一个队伍都有一个独立的实例。

在这种情况下，我们可以通过校验队伍 Token 并根据队伍 Token 来独立生成 flag，从而保证每一个队伍都有唯一的动态 flag。

### 队伍签名校验

比赛公钥可以直接从比赛管理页面获取，它是一个被 Base64 编码的 ed25519 公钥，例如：

```
s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI=
```

队伍 Token 是一个被 Base64 编码的 ed25519 签名，它的格式为：

```
1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg==
```

可以使用以下代码来校验队伍 Token，其中 `base64` 和 `nacl` 为 python 库：

```python
from base64 import b64decode
from nacl.signing import VerifyKey

token = "1201:HCdjp352NcQoL/4gS8RP3xRt5B9xX2V4m2UeoqfM2dxcLrI5FiYQ7HC9pqreG+tudWjYJf0atzQhhAKyYDKsCg=="
verify_key = VerifyKey(b64decode("s2r5WQUClYNsldJrRKanrKivBUtyN+3MjeOiKNL3znI="))

data = f"GZCTF_TEAM_{token.split(':')[0]}".encode()

try:
    verify_key.verify(data, b64decode(token.split(':')[1]))
except:
    print("Invalid token")
```

PyNaCl 是 libsodium 的 python 封装，在常见的系统中大概率已经预装了 libsodium，详情参考： [PyNaCl](https://pynacl.readthedocs.io/en/latest/)。

你也可以使用任何其他语言的 ed25519 签名校验库来校验队伍 Token 是否为平台所签发的有效签名，并为下发 flag 的安全性做密码学保证。
